package com.disk.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.disk.config.AppConfig;
import com.disk.entity.constants.Constants;
import com.disk.entity.dto.SessionWebUserDto;
import com.disk.entity.dto.UploadResultDto;
import com.disk.entity.dto.UserSpaceDto;
import com.disk.entity.enums.*;
import com.disk.entity.po.FileInfo;
import com.disk.entity.po.UserInfo;
import com.disk.entity.query.FileInfoQuery;
import com.disk.entity.query.SimplePage;
import com.disk.entity.vo.PaginationResultVO;
import com.disk.exception.BusinessException;
import com.disk.mapper.FileInfoMapper;
import com.disk.mapper.UserInfoMapper;
import com.disk.service.FileInfoService;
import com.disk.utils.ProcessUtils;
import com.disk.utils.RedisComponent;
import com.disk.utils.ScaleFilter;
import com.disk.utils.StringTools;
import org.apache.commons.io.FileUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Lazy;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.support.TransactionSynchronization;
import org.springframework.transaction.support.TransactionSynchronizationAdapter;
import org.springframework.transaction.support.TransactionSynchronizationManager;
import org.springframework.web.multipart.MultipartFile;

import javax.annotation.Resource;
import javax.xml.crypto.Data;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Date;
import java.util.List;
import java.util.Objects;

/**
 * <p>
 * 文件信息 服务实现类
 * </p>
 *
 * @author zsy
 * @since 2024-08-06
 */
@Service
public class FileInfoServiceImpl extends ServiceImpl<FileInfoMapper, FileInfo> implements FileInfoService {

    private static final Logger logger = LoggerFactory.getLogger(FileInfoServiceImpl.class);

    @Resource
    private FileInfoMapper fileInfoMapper;

    @Resource
    private RedisComponent redisComponent;

    @Resource
    private UserInfoMapper userInfoMapper;

    @Resource
    private AppConfig appConfig;


    @Resource
    @Lazy
    private FileInfoServiceImpl fileInfoServiceImpl;

    @Override
    public PaginationResultVO findListByPage(FileInfoQuery query) {
        Page<FileInfo> page = new Page<>(query.getPageSize() == null ? 15 : query.getPageSize(), query.getPageNo() == null ? 1 : query.getPageNo());
        LambdaQueryWrapper<FileInfo> wrapper = new LambdaQueryWrapper<FileInfo>()
                .eq(FileInfo::getDelFlag, FileDelFlagEnums.USING.getFlag())
                .eq(query.getFileCategory() != null, FileInfo::getFileCategory, query.getFileCategory())
                .eq(query.getUserId() != null, FileInfo::getUserId, query.getUserId())
                .eq(query.getFilePid() != null, FileInfo::getFilePid, query.getFilePid())
                .eq(query.getFileId() != null, FileInfo::getFileId, query.getFileId())
                .eq(query.getFileMd5() != null, FileInfo::getFileMd5, query.getFileMd5())
                .eq(query.getFileName() != null, FileInfo::getFileName, query.getFileName())
                .eq(query.getFilePath() != null, FileInfo::getFilePath, query.getFilePath())
                .eq(query.getFileType()!= null, FileInfo::getFileType, query.getFileType())
                .orderByDesc(FileInfo::getFolderType)
                .orderByDesc(FileInfo::getFileCategory)
                .orderByDesc(FileInfo::getFileName);

        Page<FileInfo> resultPage = fileInfoMapper.selectPage(page, wrapper);
        return new PaginationResultVO((int) resultPage.getTotal(), (int) page.getSize(), (int) page.getCurrent(), (int) page.getTotal(), resultPage.getRecords());
    }


    @Override
    @Transactional(rollbackFor = Exception.class)
    public UploadResultDto uploadFile(SessionWebUserDto webUserDto, String fileId, MultipartFile file, String fileName, String filePid, String fileMd5, Integer chunkIndex, Integer chunks) {
        UploadResultDto resultDto = new UploadResultDto();

        boolean uploadSuccess = true;

        File tempFileFolder = null;
        try {
            if (StringTools.isEmpty(fileId)) {
                fileId = StringTools.getRandomString(10);
            }
            resultDto.setFileId(fileId);

            UserSpaceDto userSpaceDto = redisComponent.getUserSpaceUse(webUserDto.getUserId());

            if (chunkIndex == 0) {
                FileInfoQuery query = new FileInfoQuery();
                query.setFileMd5(fileMd5);
                query.setSimplePage(new SimplePage(0, 1));
                query.setStatus(FileStatusEnums.USING.getStatus());
                List<FileInfo> dbfileInfoList = fileInfoMapper.selectList(new LambdaQueryWrapper<FileInfo>().eq(FileInfo::getFileMd5, fileMd5).eq(FileInfo::getStatus, FileStatusEnums.USING.getStatus()));

                //秒传的逻辑
                if (!dbfileInfoList.isEmpty()) {
                    FileInfo dbfileInfo = dbfileInfoList.get(0);
                    //判断文件的大小
                    if (dbfileInfo.getFileSize() + userSpaceDto.getUseSpace() > userSpaceDto.getTotalSpace()) {
                        throw new BusinessException(ResponseCodeEnum.CODE_904);
                    }
                    dbfileInfo.setFileId(fileId);
                    dbfileInfo.setFilePid(filePid);
                    dbfileInfo.setUserId(webUserDto.getUserId());
                    dbfileInfo.setCreateTime(LocalDateTime.now());
                    dbfileInfo.setStatus(FileStatusEnums.USING.getStatus());
                    dbfileInfo.setDelFlag(FileDelFlagEnums.USING.getFlag());
                    dbfileInfo.setFileMd5(fileMd5);
                    // 文件重命名
                    fileName = autoRename(fileName, webUserDto.getUserId(), filePid);
                    dbfileInfo.setFileName(fileName);
                    fileInfoMapper.insert(dbfileInfo);

                    resultDto.setStatus(UploadStatusEnums.UPLOAD_SECONDS.getCode());
                    //更新用户使用空间
                    updateUseSpace(webUserDto, dbfileInfo.getFileSize());
                }
            }
            // 分片上传

            //判断磁盘空间
            Long currentTempSize = redisComponent.getFileTempSize(webUserDto.getUserId(), fileId);
            if (currentTempSize + file.getSize() + userSpaceDto.getUseSpace() > userSpaceDto.getTotalSpace()) {
                throw new BusinessException(ResponseCodeEnum.CODE_904);
            }
            // 暂存临时目录
            String tempFolderName = appConfig.getProjectFolder() + Constants.FILE_FOLDER_TEMP;
            String currentUserFolderName = webUserDto.getUserId() + fileId;

            tempFileFolder = new File(tempFolderName + currentUserFolderName);

            if (!tempFileFolder.exists()) {
                tempFileFolder.mkdirs();
            }

            File newFile = new File(tempFileFolder.getPath() + "/" + chunkIndex);
            file.transferTo(newFile);
            if (chunkIndex < chunks - 1) {
                resultDto.setStatus(UploadStatusEnums.UPLOADING.getCode());
                //保存临时文件大小
                redisComponent.saveFileTempSize(webUserDto.getUserId(), fileId, file.getSize());
                return resultDto;
            }
            redisComponent.saveFileTempSize(webUserDto.getUserId(), fileId, file.getSize());
            //最后一个分片上传完成，记录数据，异步合并分片
            String month = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMM"));
            String fileSuffix = StringTools.getFileSuffix(fileName);
            //真实文件名
            String realFileName = currentUserFolderName + fileSuffix;
            FileTypeEnums fileTypeEnum = FileTypeEnums.getFileTypeBySuffix(fileSuffix);
            //自动重命名
            fileName = autoRename(fileName, webUserDto.getUserId(), filePid);
            FileInfo fileInfo = new FileInfo();
            fileInfo.setFileId(fileId);
            fileInfo.setUserId(webUserDto.getUserId());
            fileInfo.setFileMd5(fileMd5);
            fileInfo.setFileName(fileName);
            fileInfo.setFilePath(month + "/" + realFileName);
            fileInfo.setFilePid(filePid);
            fileInfo.setCreateTime(LocalDateTime.now());
            fileInfo.setLastUpdateTime(LocalDateTime.now());
            fileInfo.setFileCategory(fileTypeEnum.getCategory().getCategory());
            fileInfo.setFileType(fileTypeEnum.getType());
            fileInfo.setStatus(FileStatusEnums.TRANSFER.getStatus());
            fileInfo.setFolderType(FileFolderTypeEnums.FILE.getType());
            fileInfo.setDelFlag(FileDelFlagEnums.USING.getFlag());

            //记录在数据库
            fileInfoMapper.insert(fileInfo);

            Long totalSize = redisComponent.getFileTempSize(webUserDto.getUserId(), fileId);
            updateUseSpace(webUserDto, totalSize);
            resultDto.setStatus(UploadStatusEnums.UPLOAD_FINISH.getCode());

            TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
                @Override
                public void afterCommit() {
                    fileInfoServiceImpl.transferFile(fileInfo.getFileId(), webUserDto);
                }
            });

            return resultDto;
        } catch (Exception e) {
            logger.error("上传文件失败", e);
            uploadSuccess = false;
        } finally {
            //删除临时目录
            if (!uploadSuccess && tempFileFolder != null) {
                try {
                    FileUtils.deleteDirectory(tempFileFolder);
                } catch (IOException e) {
                    logger.error("删除临时目录失败", e);
                }
            }
        }

        return resultDto;
    }

    @Override
    public FileInfo newFolder(String userId, String filePid, String fileName) {
        checkFileName(filePid, userId, fileName, FileFolderTypeEnums.FOLDER.getType());
        FileInfo fileInfo = new FileInfo();
        fileInfo.setFileId(StringTools.getRandomString(10));
        fileInfo.setUserId(userId);
        fileInfo.setFileName(fileName);
        fileInfo.setFilePid(filePid);
        fileInfo.setCreateTime(LocalDateTime.now());
        fileInfo.setStatus(FileStatusEnums.USING.getStatus());
        fileInfo.setFolderType(FileFolderTypeEnums.FOLDER.getType());
        fileInfo.setLastUpdateTime(LocalDateTime.now());
        fileInfo.setDelFlag(FileDelFlagEnums.USING.getFlag());
        fileInfoMapper.insert(fileInfo);
        return fileInfo;
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public FileInfo rename(String userId, String fileId, String fileName) {
        FileInfo fileInfo = fileInfoMapper.selectOne(new LambdaQueryWrapper<FileInfo>()
                .eq(FileInfo::getFileId, fileId)
                .eq(FileInfo::getUserId, userId));
        if (null == fileInfo)
            throw new BusinessException("文件不存在");
        String filePid = fileInfo.getFilePid();
        checkFileName(filePid, userId, fileName, fileInfo.getFolderType());
        //获取文件后缀，如果不是文件夹，就要把后缀保留
        if (FileFolderTypeEnums.FILE.getType().equals(fileInfo.getFolderType())) {
            fileName = fileName + StringTools.getFileSuffix(fileInfo.getFileName());
        }
        fileInfo.setFileName(fileName);
        fileInfo.setLastUpdateTime(LocalDateTime.now());
        fileInfoMapper.update(fileInfo, new LambdaQueryWrapper<FileInfo>().eq(FileInfo::getFileId, fileId).eq(FileInfo::getUserId, userId));
        return fileInfo;
    }

    private void checkFileName(String filePid, String userId, String fileName, Integer fileType) {
        Long count = fileInfoMapper.selectCount(new LambdaQueryWrapper<FileInfo>()
                .eq(FileInfo::getFileName, fileName)
                .eq(FileInfo::getUserId, userId)
                .eq(FileInfo::getFilePid, filePid)
                .eq(FileInfo::getFolderType, fileType)
                .eq(FileInfo::getDelFlag, FileDelFlagEnums.USING.getFlag()));
        if (count > 0)
            throw new BusinessException("此目录下已经存在同名文件，请修改名称。");

    }

    private String autoRename(String fileName, String userId, String filePid) {
        Long count = fileInfoMapper.selectCount(new LambdaQueryWrapper<FileInfo>().eq(FileInfo::getFileName, fileName).eq(FileInfo::getUserId, userId).eq(FileInfo::getFileId, filePid).eq(FileInfo::getDelFlag, FileDelFlagEnums.USING.getFlag()));

        if (count > 0) fileName = StringTools.rename(fileName);
        return fileName;
    }


    private void updateUseSpace(SessionWebUserDto webUserDto, Long useSpace) {
        UserInfo userInfo = userInfoMapper.selectOne(new LambdaQueryWrapper<UserInfo>().eq(UserInfo::getUserId, webUserDto.getUserId()));
        UserSpaceDto userSpaceDto = redisComponent.getUserSpaceUse(webUserDto.getUserId());
        userSpaceDto.setUseSpace(userSpaceDto.getUseSpace() + useSpace);
        redisComponent.saveUserSpaceUse(webUserDto.getUserId(), userSpaceDto);
        userInfo.setUseSpace(userSpaceDto.getUseSpace());
        userInfoMapper.updateById(userInfo);
    }


    @Async
    public void transferFile(String fileId, SessionWebUserDto webUserDto) {
        Boolean transferSuccess = true;
        String targetFilePath = null;
        String cover = null;
        FileTypeEnums fileTypeEnum = null;
        FileInfo fileInfo = fileInfoMapper.selectOne(new LambdaQueryWrapper<FileInfo>().eq(FileInfo::getFileId, fileId).eq(FileInfo::getUserId, webUserDto.getUserId()));
        try {
            if (fileInfo == null || !Objects.equals(fileInfo.getStatus(), FileStatusEnums.TRANSFER.getStatus())) {
                return;
            }
            // 读取 上面建立的临时目录
            String tempFolderName = appConfig.getProjectFolder() + Constants.FILE_FOLDER_TEMP;
            String currentUserFolderName = webUserDto.getUserId() + fileId;
            File fileFolder = new File(tempFolderName + currentUserFolderName);

            String fileSuffix = StringTools.getFileSuffix(fileInfo.getFileName());
            String month = fileInfo.getCreateTime().format(DateTimeFormatter.ofPattern("yyyyMM"));

            //目标目录
            String targetFolderName = appConfig.getProjectFolder() + Constants.FILE_FOLDER_FILE;
            File targetFolder = new File(targetFolderName + "/" + month);
            if (!targetFolder.exists()) {
                targetFolder.mkdirs();
            }
            //真实的文件名
            String realFileName = currentUserFolderName + fileSuffix;

            targetFilePath = targetFolder.getPath() + "/" + realFileName;

            //合并文件
            mergeFile(fileFolder.getPath(), targetFilePath, fileInfo.getFileName(), true);

            //视频文件切割
            fileTypeEnum = FileTypeEnums.getFileTypeBySuffix(fileSuffix);
            if (fileTypeEnum.equals(FileTypeEnums.VIDEO)) {
                cutFileVideo(fileId, targetFilePath);
                //视频生成缩略图
                cover = month + "/" + currentUserFolderName + ".png";
                String coverPath = targetFolderName + "/" + cover;
                ScaleFilter.createCover4Video(new File(targetFilePath), 150, new File(coverPath));
            } else if (fileTypeEnum.equals(FileTypeEnums.IMAGE)) {
                //图片生成缩略图
                cover = month + "/" + realFileName.replace(".", "_.");
                String coverPath = targetFolderName + "/" + cover;
                Boolean c = ScaleFilter.createThumbnailWidthFFmpeg(new File(targetFilePath), 150, new File(coverPath), false);
                //原图太小就不生成缩略图，直接复制一份当缩略图
                if (!c) {
                    FileUtils.copyFile(new File(targetFilePath), new File(coverPath));
                }
            }


        } catch (Exception e) {
            logger.error("文件转码失败，文件ID：{}，userId：{}", fileId, webUserDto.getUserId(), e);
            transferSuccess = false;
        } finally {
            FileInfo updateFileInfo = new FileInfo();
            updateFileInfo.setFileSize(new File(targetFilePath).length());
            updateFileInfo.setFileCover(cover);
            updateFileInfo.setStatus(transferSuccess ? FileStatusEnums.USING.getStatus() : FileStatusEnums.TRANSFER_FAIL.getStatus());
            fileInfoMapper.update(updateFileInfo, new LambdaQueryWrapper<FileInfo>().eq(FileInfo::getFileId, fileId).eq(FileInfo::getUserId, webUserDto.getUserId()));
        }


    }

    private void mergeFile(String dirPath, String toFilePath, String fileName, Boolean delSource) {
        File dir = new File(dirPath);
        if (!dir.exists()) {
            throw new BusinessException("目录不存在");
        }
        File[] fileList = dir.listFiles();
        File targetFile = new File(toFilePath);
        RandomAccessFile writeFile = null;
        try {
            writeFile = new RandomAccessFile(targetFile, "rw");
            byte[] buffer = new byte[1024 * 10];
            for (int i = 0; i < fileList.length; i++) {
                int len = -1;
                File chunkFile = new File(dirPath + "/" + i);
                RandomAccessFile readFile = null;
                try {
                    readFile = new RandomAccessFile(chunkFile, "r");
                    while ((len = readFile.read(buffer)) != -1) {
                        writeFile.write(buffer, 0, len);
                    }
                } catch (Exception e) {
                    logger.error("合并文件分片失败", e);
                    throw new BusinessException("合并文件分片失败");
                } finally {
                    if (readFile != null) {
                        readFile.close();
                    }
                }
            }
        } catch (Exception e) {
            logger.error("合并文件：{}失败", fileName, e);
            throw new BusinessException("合并文件" + fileName + "失败");
        } finally {
            if (writeFile != null) {
                try {
                    writeFile.close();
                } catch (IOException e) {
                    logger.error("关闭文件失败", e);
                }
            }
        }
        if (delSource && dir.exists()) {
            try {
                FileUtils.deleteDirectory(dir);
            } catch (Exception e) {
                logger.error("删除源文件失败", e);
            }
        }
    }

    //视频切片功能
    private void cutFileVideo(String fileId, String videoFilePath) {
        //创建同名切片目录
        File tsFolder = new File(videoFilePath.substring(0, videoFilePath.lastIndexOf(".")));
        if (!tsFolder.exists()) {
            tsFolder.mkdirs();
        }
        //调用fmpg命令，先转为TS，再去切割
        final String CMD_TRANSFER_2TS = "ffmpeg -y -i %s -c copy -bsf:v h264_mp4toannexb %s";
        final String CMD_CUT_TS = "ffmpeg -i %s -c copy -map 0 -f segment -segment_list %s -segment_time 30 %s/%s_%%4d.ts";

        String tsPath = tsFolder + "/" + Constants.TS_NAME;

        //生成.ts
        String cmd = String.format(CMD_TRANSFER_2TS, videoFilePath, tsPath);

        ProcessUtils.executeCommand(cmd, true);
        //生成索引文件.m3u8和切片.ts文件
        cmd = String.format(CMD_CUT_TS, tsPath, tsFolder.getPath() + "/" + Constants.M3U8_NAME, tsFolder.getPath(), fileId);

        ProcessUtils.executeCommand(cmd, true);
        //删除index.ts文件
        new File(tsPath).delete();
    }

}
